/* FormView.cpp
Michael Zahniser, 01/08/09

Function definitions for the FormView class.
*/

#include "FormView.h"

#include "Interface.h"
#include "EmbeddedFont.h"

#include "_common/shift.h"
#include "_paste/Pasteboard.h"

#include <sstream>
#include <algorithm>

using std::ostringstream;
using std::max;



FormView::FormView(const string &title, bool isModal)
	: cursor(0), callback(NULL), isModal(isModal)
{
	font.Load(EmbeddedFont::DATA, EmbeddedFont::WIDTH, EmbeddedFont::HEIGHT);
	
	AddTitle(title);
	currentField = elements.begin();
}



// Set the callback for successful completion of the form.
void FormView::SetCallback(Callback &callback)
{
	this->callback = &callback;
}



// Clear all fields, and optionally reset the form title.
void FormView::Clear()
{
	elements.clear();
	currentField = elements.end();
}



void FormView::AddTitle(const string &title)
{
	elements.push_back(Element("", title));
}



void FormView::AddField(const string &label, const string &text, int min, int max)
{
	elements.push_back(Element(label, text, min, max));
}



void FormView::AddOption(const string &label, bool isChecked)
{
	elements.push_back(Element(label, isChecked ? "x" : "", 1, 0));
}



// Get the value of the field with the given label.
const string &FormView::ValueOf(const string &label) const
{
	for(list<Element>::const_iterator it = elements.begin(); it != elements.end(); ++it)
		if(it->label == label)
			return it->text;
	// The front element is a title, so it's label is guranteed to be empty.
	return elements.front().label;
}



// Get the value of the selected field.
const string &FormView::ValueOfSelection() const
{
	return currentField->text;
}



// Initialize a view, given the command line arguments for the program.
// If the view cannot be initialized, this function should return false.
// NOTE: SDL may not be initialized yet when a View's constructor is called,
// so all initialization using SDL should be done here instead.
bool FormView::Initialize(const Surface &surface)
{
	return true;
}



// Handle an event. If the event involves (x, y) coordinates, they have been
// translated such that (0, 0) is the top left corner of this View.
// This function should return false if this view does not support the given
// event and wants other views to have a chance to handle it.
bool FormView::Handle(const SDL_Event &event)
{
	if(event.type == SDL_KEYDOWN)
	{
		SDLKey key = event.key.keysym.sym;
		SDLMod mod = event.key.keysym.mod;

		// Get the modifier key state.
		bool isCommand = (mod & (KMOD_CTRL | KMOD_META)) != 0;
		bool isShifted = (mod & KMOD_SHIFT) != 0;

		if(key == SDLK_ESCAPE)
		{
			if(isModal)
				GetInterface()->Remove(this);
		}
		else if(key == SDLK_RETURN)
		{
			list<Element>::iterator selection = currentField;
			currentField = elements.begin();
			for( ; currentField != elements.end(); ++currentField)
				if(!currentField->Validate())
					break;

			if(currentField == elements.end())
			{
				currentField = selection;
				if(callback)
					(*callback)(*this);
				if(isModal)
					GetInterface()->Remove(this);
			}
			else
				cursor = static_cast<int>(currentField->text.length());
		}
		else if(key == SDLK_DOWN || key == SDLK_TAB)
		{
			if(currentField == elements.end())
				currentField = elements.begin();
			else
			{
				++currentField;
				if(currentField == elements.end())
					currentField = elements.begin();
			}
			cursor = static_cast<int>(currentField->text.length());
		}
		else if(key == SDLK_UP)
		{
			if(currentField == elements.begin())
				currentField = elements.end();
			--currentField;
			cursor = static_cast<int>(currentField->text.length());
		}
		else if(currentField == elements.end() || currentField->label.empty())
		{
			// Don't allow text editing on titles or on an empty form.
		}
		else if(isCommand && key == 'c')
		{
			Pasteboard::Set(currentField->text);
		}
		else if(isCommand && key == 'x')
		{
			Pasteboard::Set(currentField->text);
			currentField->text.clear();
			cursor = 0;
		}
		else if(isCommand && key == 'v')
		{
			string clip = Pasteboard::Get();
			currentField->text.insert(cursor, clip);
			cursor += static_cast<int>(clip.length());
		}
		else if(key == SDLK_LEFT && cursor)
			--cursor;
		else if(key == SDLK_RIGHT && cursor < static_cast<int>(currentField->text.length()))
			++cursor;
		else if((key == SDLK_DELETE || key == SDLK_BACKSPACE) && cursor)
		{
			// Don't delete if this is a radio button.
			if(currentField->min <= currentField->max)
			{
				--cursor;
				currentField->text.erase(cursor, 1);
			}
		}
		else if(!isCommand && key >= ' ' && key <= '~')
		{
			// Insert text if this is a field. If it's a radio button, activate it.
			if(currentField->min <= currentField->max)
			{
				currentField->text.insert(cursor, 1, isShifted ? SHIFT[key] : key);
				++cursor;
			}
			else
			{
				list<Element>::iterator it;
				// Loop back until the first non-radio-button element.
				for(it = currentField; it != elements.begin(); --it)
				{
					if(it->min <= it->max)
						break;
					it->text.clear();
				}
				// Loop forward until the first non-radio-button element.
				for(it = currentField; it != elements.end(); ++it)
				{
					if(it->min <= it->max)
						break;
					it->text.clear();
				}

				currentField->text = "x";
			}
		}

		// All key events are handled.
		return true;
	}

	// Absorb all other events unless this form is modeless.
	return isModal;
}



// Draw the view in the given surface. The surface is passed as a (shallow)
// copy so that it is impossible for derived classes to modify View::surface
// to point to anything other than what Interface assigns to it.
void FormView::Draw(Surface surface) const
{
	// Figure out the dimensions of the view.
	int fieldW = MIN_FIELD_WIDTH;
	int labelW = 0;
	int rows = 0;
	int constraintW = 0;
	int optionW = 0;
	int titleW = 0;

	for(list<Element>::const_iterator it = elements.begin(); it != elements.end(); ++it)
	{
		++rows;
		// If this is a title, the text fills the whole width.
		if(it->label.empty())
			titleW = max(titleW, font.Width(it->text));
		// If this is a checkbox, the box plus the label fills the whole width.
		else if(it->min > it->max)
			optionW = max(optionW, BOX_SIZE + PAD + font.Width(it->label));
		else
		{
			labelW = max(labelW, font.Width(it->label));
			fieldW = max(fieldW, font.Width(it->text));
			if(it->min < it->max)
				constraintW = max(constraintW, font.Width(it->Constraint()));
		}
	}

	fieldW += 2 * PAD;
	labelW += 2 * PAD;
	constraintW += 2 * PAD;
	optionW += 2 * PAD;
	titleW += 2 * PAD;

	int rowHeight = font.Height() + 2 + 2 * PAD;
	int boxOff = (font.Height() + 2 - BOX_SIZE) / 2;

	int width = fieldW + labelW + constraintW;
	width = max(max(width, optionW), titleW);
	width += 2 * BORDER;
	int height = 2 * BORDER + rows * rowHeight;

	int xOff = (surface.Width() - width) / 2;
	int yOff = (surface.Height() - height) / 2;

	if(isModal)
	{
		surface.FillRect(xOff, yOff, width, height, surface.Color(255));
		surface.Frame(xOff - 1, yOff - 1, width + 2, height + 2, surface.Color(0));
	}
	else
	{
		xOff = 0;
		yOff = 0;
		surface.Erase(surface.Color(255));
	}

	xOff += BORDER;
	yOff += BORDER + PAD;
	for(list<Element>::const_iterator it = elements.begin(); it != elements.end(); ++it)
	{
		int x = xOff;
		if(it == currentField)
		{
			Uint32 color = it->Validate() ? surface.Color(240) : surface.Color(255, 232, 224);
			surface.FillRect(x, yOff - PAD, width - 2 * BORDER, rowHeight, color);
		}

		// If this is a title, the text fills the whole width.
		if(it->label.empty())
			font.Draw(it->text, surface, x + PAD, yOff + 1);
		// If this is a checkbox, the box plus the label fills the whole width.
		else if(it->min > it->max)
		{
			Rectangle box(x + PAD, yOff + boxOff, BOX_SIZE, BOX_SIZE);
			if(it->text.empty())
				surface.Frame(box, surface.Color(0));
			else
				surface.FillRect(box, surface.Color(0));

			x += BOX_SIZE + 2 * PAD;
			font.Draw(it->label, surface, x, yOff + 1);
		}
		else
		{
			font.Draw(it->label, surface, x + PAD, yOff + 1);

			x += labelW;
			surface.Frame(x, yOff, fieldW, font.Height() + 2, surface.Color(0));

			font.Draw(it->text, surface, x + PAD, yOff + 1);
			if(it == currentField)
			{
				int cursorX = font.Width(it->text.begin(), it->text.begin() + cursor);
				surface.VLine(x + PAD + cursorX, yOff + 1, font.Height(), surface.Color(128));
			}

			if(it->min < it->max)
			{
				x += fieldW;
				font.Draw(it->Constraint(), surface, x + PAD, yOff + 1);
			}
		}

		yOff += rowHeight;
	}
}



string FormView::Element::Constraint() const
{
	ostringstream constraint;
	constraint << "(" << min << "-" << max << ")";
	return constraint.str();
}



bool FormView::Element::Validate() const
{
	if(min >= max)
		return true;

	int value = 0;
	string::const_iterator sit = text.begin();
	bool negate = false;
	if(sit != text.end() && *sit == '-')
	{
		negate = true;
		++sit;
	}
	for( ; sit != text.end(); ++sit)
	{
		if(*sit < '0' || *sit > '9')
			return false;
		value *= 10;
		value += *sit - '0';
	}
	if(negate)
		value = -value;
	return (value >= min && value <= max);
}
